/*
Copyright(c) 2015-2025 Panos Karabelas

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions :

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

//= INCLUDES =========
#include "common.hlsl"
//====================

// - the functions are shared between depth_prepass.hlsl, g_buffer.hlsl and depth_light.hlsl
// - this is because the calculations have to be exactly the same and therefore produce identical values over time (motion vectors) and space (depth pre-pass vs g-buffer)

// vertex buffer input
struct Vertex_PosUvNorTan
{
    float4 position                : POSITION;
    float2 uv                      : TEXCOORD;
    float3 normal                  : NORMAL;
    float3 tangent                 : TANGENT;
    min16float instance_position_x : INSTANCE_POSITION_X;
    min16float instance_position_y : INSTANCE_POSITION_Y;
    min16float instance_position_z : INSTANCE_POSITION_Z;
    uint instance_normal_oct       : INSTANCE_NORMAL_OCT;
    uint instance_yaw              : INSTANCE_YAW;
    uint instance_scale            : INSTANCE_SCALE;
};

// vertex buffer output
struct gbuffer_vertex
{
    float4 position          : SV_POSITION;
    float4 position_previous : POS_CLIP_PREVIOUS;
    float3 normal            : NORMAL_WORLD;
    float3 tangent           : TANGENT_WORLD;
    float4 uv_misc           : TEXCOORD;  // xy = uv, z = height_percent, w = instance_id - packed together to reduced the interpolators (shader registers) the gpu needs to track
    float width_percent      : TEXCOORD2; // temp, will remove
};

float4x4 compose_instance_transform(min16float instance_position_x, min16float instance_position_y, min16float instance_position_z, uint instance_normal_oct, uint instance_yaw, uint instance_scale)
{
    // compose position
    float3 instance_position = float3(instance_position_x, instance_position_y, instance_position_z);
    
    // check for identity
    if (!any(instance_position) && instance_normal_oct == 0 && instance_yaw == 0 && instance_scale == 0)
        return float4x4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
    
    // compose octahedral normal
    float x            = float(instance_normal_oct >> 8) / 255.0 * 2.0 - 1.0;
    float y            = float(instance_normal_oct & 0xFF) / 255.0 * 2.0 - 1.0;
    float3 n           = float3(x, y, 1.0 - abs(x) - abs(y));
    float mask         = step(0.0, n.z);
    float2 adjusted_xy = (float2(1.0, 1.0) - abs(n.yx)) * sign(n.xy);
    n.xy               = mask * n.xy + (1.0 - mask) * adjusted_xy;
    float3 normal      = normalize(n);
    
    // compose yaw and scale
    float yaw   = float(instance_yaw) / 255.0 * 6.28318530718; // pi_2
    float scale = exp2(lerp(-6.643856, 6.643856, float(instance_scale) / 255.0)); // log2(0.01) to log2(100)
    
    // compose quaternion
    float3 up           = float3(0, 1, 0);
    float up_dot_normal = dot(up, normal);
    float4 quat;
    if (abs(up_dot_normal) >= 0.999999)
    {
        quat = up_dot_normal > 0 ? float4(0, 0, 0, 1) : float4(1, 0, 0, 0);
    }
    else
    {
        float s = fast_sqrt(2.0 + 2.0 * up_dot_normal);
        quat    = float4(cross(up, normal) / s, s * 0.5);
    }
    float cy        = cos(yaw * 0.5);
    float sy        = sin(yaw * 0.5);
    float4 quat_yaw = float4(0, sy, 0, cy);
    float4 q        = float4(
        quat.w * quat_yaw.x + quat.x * quat_yaw.w + quat.y * quat_yaw.z - quat.z * quat_yaw.y,
        quat.w * quat_yaw.y - quat.x * quat_yaw.z + quat.y * quat_yaw.w + quat.z * quat_yaw.x,
        quat.w * quat_yaw.z + quat.x * quat_yaw.y - quat.y * quat_yaw.x + quat.z * quat_yaw.w,
        quat.w * quat_yaw.w - quat.x * quat_yaw.x - quat.y * quat_yaw.y - quat.z * quat_yaw.z
    );
    
    // compose rotation matrix
    float xx = q.x * q.x;
    float xy = q.x * q.y;
    float xz = q.x * q.z;
    float xw = q.x * q.w;
    float yy = q.y * q.y;
    float yz = q.y * q.z;
    float yw = q.y * q.w;
    float zz = q.z * q.z;
    float zw = q.z * q.w;
    float3x3 rotation = float3x3(
        1 - 2 * (yy + zz), 2 * (xy - zw), 2 * (xz + yw),
        2 * (xy + zw), 1 - 2 * (xx + zz), 2 * (yz - xw),
        2 * (xz - yw), 2 * (yz + xw), 1 - 2 * (xx + yy)
    );
    
    // compose final transform
    return float4x4(
        float4(rotation._11 * scale, rotation._12 * scale, rotation._13 * scale, 0),
        float4(rotation._21 * scale, rotation._22 * scale, rotation._23 * scale, 0),
        float4(rotation._31 * scale, rotation._32 * scale, rotation._33 * scale, 0),
        float4(instance_position, 1)
    );
}

float3 extract_position(matrix transform)
{
    return float3(transform._31, transform._32, transform._33);
}

float3x3 rotation_matrix(float3 axis, float angle)
{
    float c = cos(angle);
    float s = sin(angle);
    float t = 1.0f - c;

    axis = normalize(axis);

    return float3x3(
        t * axis.x * axis.x + c,
        t * axis.x * axis.y - s * axis.z,
        t * axis.x * axis.z + s * axis.y,
        
        t * axis.x * axis.y + s * axis.z,
        t * axis.y * axis.y + c,
        t * axis.y * axis.z - s * axis.x,
        
        t * axis.x * axis.z - s * axis.y,
        t * axis.y * axis.z + s * axis.x,
        t * axis.z * axis.z + c
    );
}

struct vertex_processing
{
    static void process_world_space(Surface surface, inout float3 position_world, inout gbuffer_vertex vertex, float3 position_local, float4x4 transform, uint instance_id, float time_offset)
    {
        float time                        = (float)buffer_frame.time + time_offset;
        float3 wind                       = buffer_frame.wind;
        float3 base_wind_dir              = normalize(wind + float3(1e-6f, 0.0f, 1e-6f));
        float base_wind_magnitude         = length(wind);
        float scaled_gust_scale           = 0.01f * (1.0f + base_wind_magnitude);        // base slow, +mag for quicker gust cycles
        float scaled_direction_time_scale = 0.05f * (1.0f + base_wind_magnitude / 2.0f); // milder scale for direction to avoid chaos
        float3 instance_up                = normalize(transform[1].xyz);
        
        // wind simulation
        if (surface.is_grass_blade() || surface.is_flower())
        {
            if (base_wind_magnitude < 0.001f)
                return;

            const float base_scale = 0.025f;                              // spatial scale for noise sampling
            const float time_scale = 0.1f * (1.0f + base_wind_magnitude); // animation speed scales with wind strength
            const float sway_amp   = base_wind_magnitude * 2.5f;          // maximum bend angle multiplier
            const float max_angle_deg = 85.0f;                            // maximum rotation angle to prevent ground intersection
            
            // height-based flexibility: stiffer at base, more flexible at tip
            float height_percent = saturate(vertex.uv_misc.z / max(GetMaterial().local_height, 0.001f));
            float height_factor  = pow(height_percent, 1.5f);
            
            // layered noise for natural sway: broad base pattern + faster gust layer
            float2 uv   = position_world.xz * base_scale + base_wind_dir.xz * time * time_scale;
            float sway  = noise_perlin(uv) * 0.7f;
            sway       += noise_perlin(uv * 2.0f + float2(time * 0.5f, 0.0f)) * 0.3f;
            sway        = sway * sway_amp * height_factor;

            // wind direction variation for natural randomness
            float dir_var   = noise_perlin(position_world.xz * 0.01f + time * 0.05f) * (PI / 6.0f);
            float3 bend_dir = normalize(base_wind_dir + float3(sin(dir_var), 0.0f, cos(dir_var)));

            // calculate maximum allowed angle: ensure blade never goes below horizontal
            // angle between blade up direction and vertical must be less than 90 degrees
            float3 vertical = float3(0.0f, 1.0f, 0.0f);
            float up_dot_vertical = dot(instance_up, vertical);
            float current_angle_from_vertical = acos(saturate(up_dot_vertical));
            float max_allowed_angle = (max_angle_deg * DEG_TO_RAD) - current_angle_from_vertical;
            max_allowed_angle = max(0.0f, max_allowed_angle);

            // rotate blade around axis perpendicular to wind direction
            float3 axis = normalize(cross(instance_up, bend_dir));
            float angle = sway * (50.0f * DEG_TO_RAD);
            
            // clamp angle to prevent ground intersection
            float angle_abs = abs(angle);
            float clamped_angle = sign(angle) * min(angle_abs, max_allowed_angle);
            
            float3x3 rot = rotation_matrix(axis, clamped_angle);

            // apply rotation to position, normal, and tangent
            float3 base_pos = position_world - position_local;
            float3 offset   = position_world - base_pos;
            position_world  = base_pos + mul(rot, offset);
            vertex.normal   = normalize(mul(rot, vertex.normal));
            vertex.tangent  = normalize(mul(rot, vertex.tangent));
        }
        else if (surface.has_wind_animation()) // tree branch/leaf wind sway
        {
            if (base_wind_magnitude < 0.001f)
                return;
                
            const float horizontal_amplitude = 0.15f;  // maximum horizontal displacement
            const float vertical_amplitude   = 0.1f;   // maximum vertical displacement
            const float sway_frequency       = 1.5f;   // slow branch sway frequency
            const float bob_frequency        = 3.0f;   // faster leaf flutter frequency
            const float noise_scale          = 0.08f;   // gust variation frequency
            const float flutter_variation    = 0.5f;    // leaf flutter intensity

            float height_factor = saturate(vertex.uv_misc.z); // 0 at trunk, 1 at branch tips

            // store original position before modifications for flutter calculation
            float3 position_original = position_world;

            // per-instance and per-vertex phase offsets for natural variation
            float instance_phase = float(instance_id) * 0.3f;
            float vertex_phase   = position_world.x * 0.05f + position_world.z * 0.05f;

            // horizontal sway: sine wave modulated by wind gusts
            float wind_phase      = time * sway_frequency + instance_phase;
            float horizontal_wave = sin(wind_phase + vertex_phase) * 0.5f;
    
            // apply gust modulation: low-frequency noise varies intensity 0.7x-1.0x
            float gust_noise = noise_perlin(float2(time * noise_scale, float(instance_id) * 0.1f));
            horizontal_wave *= (0.7f + 0.3f * gust_noise) * base_wind_magnitude;

            // apply horizontal displacement in wind direction, scaled by height
            float3 horizontal_dir     = float3(base_wind_dir.x, 0.0f, base_wind_dir.z);
            float3 horizontal_offset = horizontal_dir * horizontal_wave * horizontal_amplitude * height_factor;
            position_world          += horizontal_offset;

            // vertical oscillation: independent up-down motion
            float bob_phase   = time * bob_frequency + instance_phase * 1.2f + vertex_phase * 2.0f;
            float vertical_wave = sin(bob_phase) * 0.6f;

            // add high-frequency flutter noise for leaf movement
            float flutter_noise = noise_perlin(float2(position_original.xz * 5.0f + time * 2.0f));
            vertical_wave += flutter_noise * flutter_variation;

            // amplify vertical motion based on horizontal sway intensity
            vertical_wave *= (1.0f + 0.2f * base_wind_magnitude * abs(horizontal_wave));

            // apply vertical displacement, scaled by height
            float3 vertical_offset = float3(0.0f, vertical_wave * vertical_amplitude * height_factor * 0.8f, 0.0f);
            position_world       += vertical_offset;
            
            // update normals and tangents to reflect bending
            float bend_amount = length(horizontal_offset + vertical_offset) * 0.5f * height_factor;
            float3 bend_dir   = normalize(horizontal_offset + vertical_offset * 0.5f);
            vertex.normal   += bend_dir * bend_amount;
            vertex.normal    = normalize(vertex.normal);
            vertex.tangent  += bend_dir * bend_amount * 0.5f;
            vertex.tangent   = normalize(vertex.tangent);
        }
    }
};

gbuffer_vertex transform_to_world_space(Vertex_PosUvNorTan input, uint instance_id, matrix transform, inout float3 position_world, inout float3 position_world_previous)
{
    MaterialParameters material = GetMaterial();
    Surface surface;
    surface.flags = material.flags;

    gbuffer_vertex vertex;
    vertex.uv_misc.w = instance_id;
    
    // compute UV with tiling and offset
    float2 uv = input.uv * material.tiling + material.offset;
    
    // apply UV inversion: mirror along axis if enabled
    float2 invert_mask = step(0.5f, material.invert_uv);
    uv = lerp(uv, 1.0f - frac(uv) + floor(uv), invert_mask);
    vertex.uv_misc.xy = uv;
    
    // compute width and height percent for grass blade positioning
    float width_percent  = saturate((input.position.x + material.local_width * 0.5f) / material.local_width);
    float height_percent = saturate(input.position.y / material.local_height);
    vertex.uv_misc.z    = height_percent;
    vertex.width_percent = width_percent;
    
    // compose instance transform and apply to base transform
    matrix instance = compose_instance_transform(input.instance_position_x, input.instance_position_y, input.instance_position_z, input.instance_normal_oct, input.instance_yaw, input.instance_scale);
    transform = mul(instance, transform);
    matrix transform_previous = mul(instance, pass_get_transform_previous());
    
    // transform position to world space
    float3 position          = mul(input.position, transform).xyz;
    float3 position_previous = mul(input.position, transform_previous).xyz;
    
    // transform normal and tangent to world space
    float3x3 transform_3x3 = (float3x3)transform;
    vertex.normal  = normalize(mul(input.normal, transform_3x3));
    vertex.tangent = normalize(mul(input.tangent, transform_3x3));

    // apply wind animation and other world-space effects
    vertex_processing::process_world_space(surface, position, vertex, input.position.xyz, transform, instance_id, 0.0f);
    vertex_processing::process_world_space(surface, position_previous, vertex, input.position.xyz, transform_previous, instance_id, -buffer_frame.delta_time);

    position_world          = position;
    position_world_previous = position_previous;
    return vertex;
}

gbuffer_vertex transform_to_clip_space(gbuffer_vertex vertex, float3 position, float3 position_previous)
{
    vertex.position          = mul(float4(position, 1.0f), buffer_frame.view_projection);
    vertex.position_previous = mul(float4(position_previous, 1.0f), buffer_frame.view_projection_previous);
    
    return vertex;
}
